﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Net;
using System.Net.NetworkInformation;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Text;
using System.Xml;
using System.Xml.Linq;
using System.Xml.Schema;
using System.Xml.Serialization;
using NLog;
using DeSleeper.Library;
using DeSleeper.Service.Properties;

namespace DeSleeper.Service
{
    /// <summary>
    /// Cache of information necessary to wake up a target.
    /// </summary>
    public class TargetCache : IXmlSerializable, IEquatable<TargetCache>
	{
		private static readonly Logger _logger = LogManager.GetCurrentClassLogger();
		
		private static TargetCache _instance;
		public static TargetCache Instance
		{
			get
			{
                if (_instance == null)
                {
                    _instance = Settings.Default.TargetCache ?? new TargetCache();
                    _instance.WakeUpUnregisteredDestinations = Settings.Default.WakeUpUnregisteredDestinations;
                }

			    return _instance;
			}
		}

		public static void Save()
		{
			Settings.Default.TargetCache = Instance;
            Settings.Default.WakeUpUnregisteredDestinations = Instance.WakeUpUnregisteredDestinations;
			Settings.Default.Save();
		}

		private readonly Dictionary<string, HostInfo> _cache = new Dictionary<string, HostInfo>();
		public bool WakeUpUnregisteredDestinations { get; set; }

		public TargetCache() {}
		public TargetCache(Dictionary<string, HostInfo> cache)
		{
			_cache = cache;
		}

		public bool Contains(string hostName)
		{
		    return _cache.ContainsKey(hostName);
		}

	    /// <summary>
	    /// Finds a machines network information using a host name.
	    /// </summary>
	    /// <param name="hostName">The host name or IP Address of the target.</param>
	    /// <param name="wakeUpUnregisteredDestinations">
	    /// Attempt to find the network information of a machine if not previously cached?
	    /// </param>
	    /// <param name="hostInfo"><see cref="HostInfo"/> of the machine if found.</param>
	    /// <returns>True if information could be found, false otherwise.</returns>
	    /// <remarks>
	    /// If a machine is not off it may not be possible to find information not already
	    /// in the cache.
	    /// </remarks>
	    public bool TryGetValue(string hostName, bool wakeUpUnregisteredDestinations, out HostInfo hostInfo)
        {
            if (_cache.TryGetValue(hostName, out hostInfo)) return true;
            if (!wakeUpUnregisteredDestinations) return false;


	        IPHostEntry hostEntry;
	        try
	        {
	            hostEntry = Dns.GetHostEntry(hostName);
	        }
	        catch (SocketException e)
	        {
                if (e.SocketErrorCode == SocketError.HostNotFound) return false;
	            else throw;
	        }
	        var ipAddress = hostEntry.AddressList.FirstOrDefault(address => address.AddressFamily == AddressFamily.InterNetwork);
            if (ipAddress == null)
                return false;

            var macAddress = new byte[6];
            var macAddressLength = (uint) macAddress.Length;

#pragma warning disable 618,612
            if (SendARP((int) ipAddress.Address, 0, macAddress, ref macAddressLength) != 0)
#pragma warning restore 618,612
                return false;

            hostInfo = new HostInfo {Current = ipAddress, Subnet = IPAddress.Any, MacAddress = new PhysicalAddress(macAddress)};
            AddHostInfo(hostName, hostInfo);
            return true;
        }

	    /// <summary>
        /// Add an entry to the cache of known machines.
	    /// </summary>
        /// <param name="hostName">Host name for cached machine.</param>
	    /// <param name="current">The current IP Address to store in the cache.</param>
	    /// <param name="macAddress">MAC Address of the machine.</param>
	    /// <param name="subnet">Current subnet of the store in the cache.</param>
	    public void AddHostInfo(string hostName, IPAddress current, string macAddress, IPAddress subnet)
		{
			AddHostInfo(hostName, new HostInfo { Current = current, MacAddress = PhysicalAddress.Parse(macAddress), Subnet = subnet });
		}

	    /// <summary>
	    /// Add an entry to the cache of known machines.
	    /// </summary>
	    /// <param name="hostName">Host name for cached machine.</param>
	    /// <param name="hostInfo">Network information to store in the cache.</param>
	    public void AddHostInfo(string hostName, HostInfo hostInfo)
		{
			lock (_cache)
			{
				HostInfo oldInfo;
				if (_cache.TryGetValue(hostName, out oldInfo))
				{
					_cache.Remove(hostName);
					_logger.Info("Host information is being updated.  Old data: {0}, {1}, {2}/{3}", hostName, oldInfo.MacAddress, oldInfo.Current, oldInfo.Subnet);
				}

				_cache.Add(hostName, hostInfo);
				_logger.Info("Host information has been added or updated.  New data: {0}, {1}, {2}/{3}", hostName, hostInfo.MacAddress, hostInfo.Current, hostInfo.Subnet);
				Save();
			}
		}


	    /// <summary>
	    /// Wake up machine using a host name using cached MAC Address or local network
	    /// information.
	    /// </summary>
	    /// <param name="hostName">Host name of the machine to wake up.</param>
	    /// <exception cref="InvalidOperationException">
	    /// Unregistered host, wake up failed.
	    /// </exception>
	    public void WakeUpRegisteredHost(string hostName)
		{
			HostInfo hostInfo;
			if (!TryGetValue(hostName, WakeUpUnregisteredDestinations, out hostInfo))
				throw new InvalidOperationException("Unregistered host, wake up failed.");

	        var addresses = from intf in NetworkInterface.GetAllNetworkInterfaces()
	                        from address in intf.GetIPProperties().UnicastAddresses
	                        where intf.OperationalStatus == OperationalStatus.Up &&
	                              address.Address.AddressFamily == AddressFamily.InterNetwork
	                        select address;
#pragma warning disable 618,612
            var canDispatchLocally = addresses.Any(address => 
												   (address.Address.Address & address.IPv4Mask.Address) == (hostInfo.Current.Address & address.IPv4Mask.Address));
#pragma warning restore 618,612


			if (canDispatchLocally)
			{
				WakeOnLanHelper.WakeUp(hostInfo.MacAddress);
			}
			else
			{
				// Find host in cache that can wake up.
			}
		}

		#region IXmlSerializable
		XmlSchema IXmlSerializable.GetSchema()
		{
			return null;
		}

		void IXmlSerializable.ReadXml(XmlReader reader)
		{
			var element = XNode.ReadFrom(reader) as XElement;
			foreach (var child in element.Elements("Entry"))
			{
				var key = child.Element("HostName");
				var hostInfo = new HostInfo();
				var serializable = hostInfo as IXmlSerializable;
				serializable.ReadXml(child.Element("Value").CreateReader());
				_cache.Add(key.Value, hostInfo);
			}
		}

		void IXmlSerializable.WriteXml(XmlWriter writer)
		{
			foreach (var entry in _cache)
			{
				writer.WriteStartElement("Entry");
				writer.WriteElementString("HostName", entry.Key);
				var hostInfo = entry.Value as IXmlSerializable;
				writer.WriteStartElement("Value");
				hostInfo.WriteXml(writer);
				writer.WriteEndElement();
				writer.WriteEndElement();
			}
		}
		#endregion

		#region Equality operations
		public bool Equals(TargetCache other)
		{
			if (ReferenceEquals(null, other)) return false;
			if (ReferenceEquals(this, other)) return true;
			return other._cache.SequenceEqual(_cache);
		}

		public override bool Equals(object obj)
		{
			if (ReferenceEquals(null, obj)) return false;
			if (ReferenceEquals(this, obj)) return true;
			if (obj.GetType() != typeof (TargetCache)) return false;
			return Equals((TargetCache) obj);
		}

		public override int GetHashCode()
		{
			return (_cache != null ? _cache.GetHashCode() : 0);
		}
		#endregion

		[DllImport("iphlpapi.dll", ExactSpelling=true)]
		private static extern int SendARP( int destIP, int srcIP, [Out] byte[] pMacAddr, ref uint phyAddrLen );
	}
}
